package org.libarae.server;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.libarae.communication.common.Book;
import org.libarae.communication.common.Book.BookStatus;
import org.libarae.communication.common.Message;
import org.libarae.communication.common.MessageThread;
import org.libarae.communication.common.User;
import org.libarae.server.databases.ExternalDBs;
import org.libarae.server.exceptions.HashException;

/**
 * @author Tyler Holzer
 *
 * DataManager.java -- part of Libarae
 * a class to manage access to the various data storage mediums
 * once started, the DataManager thread will remove users who have been
 * inactive for five minutes every five minutes
 */
public class DataManager extends Thread {
	
	/**
	 * the link with the sqlite or mySql database
	 */
	private Database db=null; //The database
	/**
	 * a link to ISBNdb and OpenLib
	 */
	private ExternalDBs external=null;
	/**
	 * the hash table of active users and their keys
	 */
	private Hashtable<String,ActiveUser> activeUsers = null; //holds currently connected users
	
	/**
	 * Constructor for DataManager
	 * 
	 * @param exists -- states weather or not the database exists
	 * @param type -- the type of the database
	 */
	public DataManager(boolean exists, int type) {
		activeUsers = new Hashtable<String,ActiveUser>();
		db=new Database(type);
		if(!exists) {
			db.deleteDB();//Make sure the database is empty
			db.createDB();
		}
		external = new ExternalDBs();
	}
	
	/* (non-Javadoc)
	 * @see java.lang.Thread#run()
	 * 
	 * runs through the list of users and removes anyone without activity in the last 5 minutes
	 */
	public void run() {
		while(true) {
			List<String> toBeRemoved=new ArrayList<String>(); //this is used to prevent concurency issues
			synchronized(activeUsers) {
				Collection<ActiveUser> c=activeUsers.values();
				Iterator<ActiveUser> iter=c.iterator();
				long currentTime=System.currentTimeMillis();
				while(iter.hasNext()) {
					ActiveUser tmp=iter.next();
					if(tmp.getTimestamp()<currentTime-300000) { // if the time is greater than 5 minutes, add the user to the list to be removed
						if(Server.config.inDebugMode())//DEBUG MESSAGE
							System.out.println(tmp.getUser().getUserName()+" has been removed"); 
						toBeRemoved.add(tmp.getAuthKey());
					}
				}
			}
			for(int i=0;i<toBeRemoved.size();i++){
				removeFromHashmap(toBeRemoved.get(i));
			}
			try {
				Thread.sleep(300000); // sleep for 5 minutes
			} 
			catch (InterruptedException e) { 
				if(Server.config.inDebugMode())//DEBUG MESSAGE
					e.printStackTrace();
			}
		}
	}
	
	/**
	 * removes a user from the hash map, and if there are no users, close the database
	 * 
	 * @param authKey -- the auth key of the user you want to remove
	 */
	public void removeFromHashmap(String authKey) {
		synchronized(activeUsers) {
			activeUsers.remove(authKey);
			if(listIsEmpty()) {
				if(Server.config.inDebugMode())//DEBUG MESSAGE
					System.out.println("Closing DB");
				db.close();
			}
		}
	}
	
	/**
	 * returns whether or not the list is empty
	 * 
	 * @return boolean
	 */
	public boolean listIsEmpty() {
		if(activeUsers.size()==0) {
			return true;
		}
		return false;
	}
	
	/**
	 * Adds a users to the list of active users
	 * user is added with to the HashTable with authKey as the key
	 * 
	 * @param authKey -- the key for the hashkey
	 * @param user -- the user to add to the hashMap
	 */
	public void addToHashmap(String authKey, User user) {
		synchronized(activeUsers) {
			activeUsers.put(authKey, new ActiveUser(user,authKey));
		}
	}
	
	/**
	 * Checks if the authKey is in the database, if it is
	 * returns the User associated with it
	 * 
	 * @param authKey -- the auth key you wish to get the user with
	 * @return User or null
	 */
	public User checkAuth(String authKey) {
		ActiveUser tmp=null;	
		if(authKey!=null) {
			tmp=activeUsers.get(authKey);
		}
			if(tmp==null)
				return null; //returns null if the authkey was not in the HashTable
			else {
				tmp.nowActive();
				return tmp.getUser(); //return the User object
			}
	}
	
	/**
	 * logins the username and password provided that they match what is in the database
	 * 
	 * @param login -- the username of the user intended to be logged in
	 * @param password -- the password that is associated with the login
	 * @return null if password or user is invalid, and the Authkey if not
	 * @throws HashException thrown when an error occurs while hashing authkey
	 * @throws SQLException thrown when an error other than bad username or pass is encounted
	 */
	public ActiveUser login(String login, String password) throws HashException, SQLException {
		if(Server.config.inDebugMode())//DEBUG MESSAGE
			System.out.println("requesting login Username: "+login+" pass: "+password);
		synchronized(activeUsers) {
			if(listIsEmpty()) {
				if(Server.config.inDebugMode())//DEBUG MESSAGE
					System.out.println("opening db");
				db.open();
			}
		}
		User tmp=db.userLogin(login, password);
		if(tmp!=null) {
			String auth=getMd5(System.currentTimeMillis()+password); //create a hash key with the hashed password and the time
			if(auth!=null) {
				addToHashmap(auth, tmp);
				return new ActiveUser(tmp,auth); //return the hash key
			}
			else {
				throw new HashException(); //throw a hash exception if an error occured in the 
			}
		}
		return null; //invalid user or pass
	}

	/**
	 * takes an input string and turns it into a MD5 hash
	 * 
	 * @param pInput -- the string you wish to convert to a hash
	 * @return the hash of pInput
	 */
	public String getMd5(String pInput){
		try {
			MessageDigest lDigest = MessageDigest.getInstance("MD5");
			lDigest.update(pInput.getBytes());
			BigInteger lHashInt = new BigInteger(1, lDigest.digest());
			return String.format("%1$032X", lHashInt);
		} catch(NoSuchAlgorithmException lException) {
			if(Server.config.inDebugMode())//DEBUG MESSAGE
				lException.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Adds the User to the database, and logs them into the server
	 * 
	 * @param user -- the user that you wish to add to the database
	 * @return Null if the username is taken, or the authkey if not
	 * @throws HashException if a error occurred in the hash
	 * @throws SQLException thrown if an error was uncounted in the DB
	 */
	public String addNewUser(User user) throws HashException, SQLException {
		if(Server.config.inDebugMode())//DEBUG MESSAGE
			System.out.println("requesting Add Username: "+user.getUserName()+" pass: "+user.getPassword());
		user.setStatus(User.Permission.USER); //all new users are lowest permision value
		synchronized(activeUsers) {
			if(listIsEmpty()) {
				if(Server.config.inDebugMode())//DEBUG MESSAGE
					System.out.println("Opening DB");
				db.open();
			}
		}
		user=db.addUser(user);
		if(user!=null) {
			String auth=getMd5(System.currentTimeMillis()+user.getPassword());
			if(auth!=null) {
				addToHashmap(auth, user);
				return auth; //Returns the auth for the user
			}
			else {
				throw new HashException(); //hash exception occurred
			}
		}
		return null; //User name taken
	}
	
	/**
	 * searches the database based on the provided panel
	 * for any field you don't wish to specify send an empty string ""
	 * 
	 * @param title -- the string you want to look for in the title of the book 
	 * @param isbn -- The string you want to look for in the isbns of the book
	 * @param author -- the string you want to look for in the author of the book
	 * @param summary -- the string you want to look for in the summary of the book
	 * @param publisher -- the string you want to look for in the publisher of the book
	 * @param omni -- the string you want to look for in all fields of the book
	 * @param start -- the start value you wish to retrieve?
	 * @param end -- the end value for the search reasults
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> bookSearch(String title, String isbn, String author, String summary, String publisher, String omni, BookStatus status, String pubDate, int start, int end) {
		return db.search(title, isbn, author, summary, publisher, omni,status, pubDate, start,end);
	}
	
	/**
	 * returns the count of unread messages associated with the UserId
	 * 
	 * @param userID -- the user ID that the messages should be related to
	 * @return int
	 */
	public int unReadMessages(int userID) {
		return db.getUnreadMessageCount(userID);
	}
	
	/**
	 * gets a list of books which were added to the library most recently
	 * 
	 * @param numberOfBooks -- the number of books to return
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> recentBooks(int numberOfBooks) {
		return db.getMostRecentBooks(numberOfBooks);
	}
	
	/**
	 * returns the books with the most checkouts
	 * 
	 * @param numberOfBooks -- the number of books to return
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> activeBooks(int numberOfBooks) {
		return db.getBooksWithMostCheckout(numberOfBooks);
	}
	
	/**
	 * returns the Book associated with the bookid
	 * 
	 * @param bookId -- the ID of the book
	 * @return Book
	 */
	public Book getBookInfo(int bookId) {
		return db.getBook(bookId);
	}
	
	/**
	 * returns the Books that the user associated with userID
	 * 
	 * @param userID -- the ID of the USer
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> usersBooks(int userID) {
		return db.getBooksOwnedBy(userID);
	}
	
	/**
	 * returns the list of books checked out by the user associated with userID
	 * 
	 * @param userID -- The ID of the User
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> checkedOutBooks(int userID) {
		return db.getBooksLoaning(userID, 0, 5);
	}
	
	/**
	 * Returns data about the book related too by the ISBN
	 * It is retrieved from and external source
	 * 
	 * @param isbn -- the Isbn of the book
	 * @return Book
	 */
	public Book isbnLookUp(String isbn) {
		Book tmp=external.getBookInfo(isbn, Server.config.getAccessKey());
		return tmp;
	}
	
	/**
	 * Creates a new book entry in the database
	 * 
	 * @param b -- the book you want to add to the database
	 * @return boolean
	 */
	public boolean createBook(Book b) {
		return db.addBook(b);
	}
	
	/**
	 * Returns the Message Threads associated with the user
	 * 
	 * @param u -- the user associated
	 * @param start -- the start value
	 * @param end -- the end value
	 * @return ArrayList<MessageThread>
	 */
	public ArrayList<MessageThread> getMessageThreads(User u, int start, int end) {
		return db.getThreads(u.getUID()); //TODO eventually fix
	}
	
	/**
	 * Logouts the user associated with the auth key from the logout
	 * 
	 * @param auth -- the authkey
	 */
	public void logout(String auth) {
		synchronized(activeUsers) {
			removeFromHashmap(auth);
		}
	}
	
	/**
	 * updates the User associated with based on the uid and returns the updated data
	 * 
	 * @param u -- the new set of data that the user should feature
	 * @return User
	 */
	public User updateUser(User u) {
		if(db.updateUser(u))
			return u;
		else
			return null;
	}
	
	/**
	 * Removes the user from the database and loges them out
	 * 
	 * @param u -- the User to be removed
	 * @param auth -- the auth key associated with the user
	 * @return boolean
	 */
	public boolean removeUser(User u,String auth) {
		boolean b=db.deleteUser(u);
		logout(auth);
		return b;
	}
	
	/**
	 * Stores a new message and messageThread in the database
	 * 
	 * @param m -- the message you want to commit
	 * @param t -- the new thread you wish to commit
	 * @return boolean
	 */
	public boolean sendNewMessage(Message m, MessageThread t) {
		return db.newMessage(t, m);
	}
	
	/**
	 * adds the message m to the database
	 * 
	 * TODO change to returning the added message with message ID
	 * 
	 * @param m -- the message that you want to do
	 * @return boolean
	 */
	public boolean sendMessage(Message m) {
		return db.addMessageToThread(m); 
	}
	
	/**
	 * returns the message which is set on the server
	 * 
	 * @return String
	 */
	public String getServerMessage() {
		return db.getServerMessage();
	}
	
	/**
	 * gets the Server password hash
	 * 
	 * @return String
	 */
	public String getServerPassword() {
		return db.getServerPassword();
	}
	
	/**
	 * gets all books in the database from start to end
	 * 
	 * @param start -- the value to start the results at
	 * @param end -- the value to end the results at
	 * @return ArrayList<Book>
	 */
	public ArrayList<Book> getBooks(int start, int end) {
		return db.getAllBooks(start, end);
	}
	
	/**
	 * gets the users in the database from start to end
	 * 
	 * @param start -- the value to start the results at
	 * @param end -- the value to end the results at
	 * @return ArryaList<User>
	 */
	public ArrayList<User> getUsers(int start, int end) {
		return db.getAllUsers(start, end); 
	}
	
	/**
	 * Removes the book associated with bId from database
	 * 
	 * @param bId -- the bookID
	 * @return boolean
	 */
	public boolean removeBook(int bId) {
		return db.deleteBook(bId);
	}
	
	/**
	 * removes the user associated with uId
	 * 
	 * @param uId -- the User ID
	 * @return boolean
	 */
	public boolean removeUser(int uId) {
		return db.deleteUser(uId);
	}
	
	/**
	 * sets the server password to s
	 * 
	 * @param s -- the new server password
	 * @return boolean
	 */
	public boolean setServerPassword(String s) {
		return db.setServerPassword(s);
	}
	
	/**
	 * sets the Server Message to string s
	 * 
	 * @param s -- the new message
	 * @return boolean
	 */
	public boolean setServerMessage(String s) {
		return db.setServerMessage(s);
	}
	
	/**
	 * sets the message marked by messageID to isRead
	 * 
	 * @param isRead -- boolean value of true or false if read
	 * @param messageID -- the message id
	 * @return boolean
	 */
	public boolean setThreadRead(boolean isRead, int messageID) {
		return db.markRead(messageID, isRead);
	}
	
	/**
	 * changes the status of the book to status and checked out by to the valuse passed
	 * 
	 * @param bid -- the book id
	 * @param status -- the new status
	 * @param checkedOutBy -- who is checking it out, or -1 if not
	 * @return boolean
	 */
	public boolean changedBookStatus(int bid, Book.BookStatus status, int checkedOutBy) {
		
		return db.updateBookStatus(bid, status, checkedOutBy);
	}
	
	/**
	 * get Messages associated with the thead associated with thread ID
	 * 
	 * @param threadId -- the thread id
	 * @return ArrayList<Message>
	 */
	public ArrayList<Message> getMessages(int threadId) {
		return db.getMessages(threadId);
	}
	
	/**
	 * closes the database
	 */
	public void closeDB() {
		if(Server.config.inDebugMode())//DEBUG MESSAGE
			System.out.println("Closing DB");
		db.close();
	}
	
	/**
	 * gets the message ID for the first message in the thread
	 * 
	 * @param threadId -- the thread ID
	 * @return int
	 */
	public int getFirstMessageID(int threadId) {
		return db.getFirstMessageInThread(threadId);
	}
	
	/**
	 * Changes the pending status of the message 
	 * 
	 * @param messageId -- the messageID of the message
	 * @param pending -- boolean for pending to be set too
	 * @return boolean
	 */
	public boolean setPendingStatus(int messageId, boolean pending) {
		return db.setIsPending(messageId, pending);
	}
	
	/**
	 * Edits the book with the same bid as b
	 * 
	 * @param b -- the new data to be stored
	 * @return false
	 */
	public boolean editBook(Book b) {
		return db.updateBook(b);
	}
}
